# frozen_string_literal: true

require_relative '../../../spec_helper'

require 'wpxf/cli/context'
require 'wpxf/cli/loaded_module'

describe Wpxf::Cli::LoadedModule do
  let :subject do
    Class.new do
      include Wpxf::Cli::LoadedModule
    end.new
  end

  let(:mod) { Wpxf::Module.new }

  before :each, 'setup mocks' do
    allow(subject).to receive(:print_warning)
    allow(subject).to receive(:print_bad)
    allow(subject).to receive(:print_good)
    allow(subject).to receive(:context).and_return(Wpxf::Cli::Context.new)

    allow(mod).to receive(:aux_module?).and_return(true)
    mod.options = []
    mod.register_options(
      [
        Wpxf::StringOption.new(name: 'test1', required: true),
        Wpxf::StringOption.new(name: 'test2', required: false),
        Wpxf::StringOption.new(name: 'test3', required: true)
      ]
    )

    allow(subject.context).to receive(:module).and_return(mod)
  end

  describe '#module_loaded?' do
    context 'if a context is not on the stack' do
      context 'and `quiet is false`' do
        it 'should print a warning' do
          allow(subject).to receive(:context).and_return(nil)
          subject.module_loaded?(false)
          expect(subject).to have_received(:print_warning)
            .with('No module loaded')
            .exactly(1).times
        end
      end

      it 'should return false' do
        expect(subject.module_loaded?).to be true
      end
    end

    context 'if a context is on the stack' do
      it 'should return true' do
        expect(subject.module_loaded?).to be true
      end
    end
  end

  describe '#module_can_execute?' do
    context 'if the module can execute' do
      it 'should return true' do
        mod.set_option_value('test1', 'test')
        mod.set_option_value('test3', 'test')
        expect(subject.module_can_execute?).to be true
      end
    end

    context 'if the module cannot execute' do
      it 'should print a list of the required options that have not been set' do
        subject.module_can_execute?
        expect(subject).to have_received(:print_bad)
          .with('One or more required options not set: test1, test3')
          .exactly(1).times

        mod.set_option_value('test1', 'test')
        subject.module_can_execute?
        expect(subject).to have_received(:print_bad)
          .with('One or more required options not set: test3')
          .exactly(1).times
      end

      it 'should return false' do
        expect(subject.module_can_execute?).to be false
      end
    end
  end

  describe '#payload_prepared?' do
    context 'if a payload is not loaded' do
      it 'should return true' do
        expect(subject.payload_prepared?).to be true
      end
    end

    context 'if a payload is loaded' do
      let(:payload) { double('payload') }

      before :each, 'setup payload' do
        mod.payload = payload
      end

      context 'if `payload.prepare` returns true' do
        it 'should return true' do
          allow(payload).to receive(:prepare).and_return(true)
          expect(subject.payload_prepared?).to be true
        end
      end

      context 'if `payload.prepare` returns false' do
        before :each, 'setup mocks' do
          allow(payload).to receive(:prepare).and_return(false)
        end

        it 'should report an error' do
          subject.payload_prepared?
          expect(subject).to have_received(:print_bad)
            .with('Failed to prepare the payload')
            .exactly(1).times
        end

        it 'should return false' do
          expect(subject.payload_prepared?).to be false
        end
      end
    end
  end

  describe '#execute_module' do
    context 'if no payload is loaded' do
      context 'and `module.run` returns true' do
        it 'should return true' do
          allow(mod).to receive(:run).and_return(true)
          expect(subject.execute_module).to be true
        end
      end

      context 'and `module.run` returns false' do
        it 'should return false' do
          allow(mod).to receive(:run).and_return(false)
          expect(subject.execute_module).to be false
        end
      end
    end

    context 'if a payload is loaded' do
      let(:payload) { double('payload') }

      before :each, 'setup mocks' do
        mod.payload = payload
      end

      context 'and `module.run` returns true' do
        before :each, 'setup mocks' do
          allow(mod).to receive(:run).and_return(true)
        end

        context 'and `payload.post_exploit` returns true' do
          it 'should return true' do
            allow(payload).to receive(:post_exploit).and_return(true)
            expect(subject.execute_module).to be true
          end
        end

        context 'and `payload.post_exploit` returns false' do
          it 'should return false' do
            allow(payload).to receive(:post_exploit).and_return(false)
            expect(subject.execute_module).to be false
          end
        end
      end

      context 'and `module.run` returns false' do
        before :each, 'setup mocks' do
          allow(mod).to receive(:run).and_return(false)
        end

        context 'and `payload.post_exploit` returns true' do
          it 'should return false' do
            allow(payload).to receive(:post_exploit).and_return(true)
            expect(subject.execute_module).to be false
          end
        end

        context 'and `payload.post_exploit` returns false' do
          it 'should return false' do
            allow(payload).to receive(:post_exploit).and_return(false)
            expect(subject.execute_module).to be false
          end
        end
      end
    end

    context 'if an error is raised' do
      before :each, 'setup mocks' do
        allow(mod).to receive(:run).and_raise(StandardError.new('test'))
      end

      it 'should print an error message' do
        subject.execute_module
        expect(subject).to have_received(:print_bad).exactly(2).times
        expect(subject).to have_received(:print_bad)
          .with('Uncaught error: test')
          .exactly(1).times
      end

      it 'should return false' do
        expect(subject.execute_module).to be false
      end
    end
  end

  describe '#run' do
    let(:module_loaded?) { true }
    let(:module_can_execute?) { true }
    let(:payload_prepared?) { true }
    let(:execute_module) { true }

    before :each, 'setup mocks' do
      allow(subject).to receive(:module_loaded?).and_return(module_loaded?)
      allow(subject).to receive(:module_can_execute?).and_return(module_can_execute?)
      allow(subject).to receive(:payload_prepared?).and_return(payload_prepared?)
      allow(subject).to receive(:execute_module).and_return(execute_module)
      allow(subject).to receive(:indent_level=)
      allow(mod).to receive(:cleanup)
    end

    context 'if the module fails to load' do
      let(:module_loaded?) { false }

      it 'should not execute the module' do
        subject.run
        expect(subject).to_not have_received(:execute_module)
      end
    end

    context 'if the module cannot execute' do
      let(:module_can_execute?) { false }

      it 'should not execute the module' do
        subject.run
        expect(subject).to_not have_received(:execute_module)
      end
    end

    context 'if the payload did not prepare' do
      let(:payload_prepared?) { false }

      it 'should not execute the module' do
        subject.run
        expect(subject).to_not have_received(:execute_module)
      end
    end

    context 'if the module executed' do
      it 'should invoke #cleanup on the module' do
        subject.run
        expect(mod).to have_received(:cleanup).exactly(1).times
      end

      it 'should reset the indent level to 1' do
        subject.run
        expect(subject).to have_received(:indent_level=)
          .with(1)
          .exactly(1).times
      end
    end

    context 'if the module successfully executes' do
      it 'should print a success message' do
        subject.run
        expect(subject).to have_received(:print_good)
          .with('Execution finished successfully')
          .exactly(1).times
      end
    end

    context 'if the module fails to execute' do
      let(:execute_module) { false }

      it 'should print an error message' do
        subject.run
        expect(subject).to have_received(:print_bad)
          .with('Execution failed')
          .exactly(1).times
      end
    end
  end

  describe '#check' do
    let(:module_loaded?) { true }
    let(:module_can_execute?) { true }
    let(:check_result) { :vulnerable }

    before :each, 'setup mocks' do
      allow(subject).to receive(:module_loaded?).and_return(module_loaded?)
      allow(subject).to receive(:module_can_execute?).and_return(module_can_execute?)
      allow(mod).to receive(:check).and_return(check_result)
    end

    context 'if the module fails to load' do
      let(:module_loaded?) { false }

      it 'should not check the target' do
        subject.check
        expect(mod).to_not have_received(:check)
      end
    end

    context 'if the module cannot execute' do
      let(:module_can_execute?) { false }

      it 'should not check the target' do
        subject.check
        expect(mod).to_not have_received(:check)
      end
    end

    context 'if the target is vulnerable' do
      it 'should print a warning' do
        subject.check
        expect(subject).to have_received(:print_warning)
          .with('Target appears to be vulnerable')
          .exactly(1).times
      end
    end

    context 'if the status of the target is unknown' do
      let(:check_result) { :unknown }

      it 'should print an error' do
        subject.check
        expect(subject).to have_received(:print_bad)
          .with('Could not determine if the target is vulnerable')
          .exactly(1).times
      end
    end

    context 'if the target is safe' do
      let(:check_result) { :safe }

      it 'should print a success message' do
        subject.check
        expect(subject).to have_received(:print_good)
          .with('Target appears to be safe')
          .exactly(1).times
      end
    end
  end
end
